﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using Skugo.Shared;
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using System.Runtime.InteropServices;

namespace Skugo
{
    public enum MouseButton
    {
        Left,
        Middle,
        Right,
    }

    public class MouseEvent : EventArgs
    {
        public Boolean LeftButton;
        public Boolean MiddleButton;
        public Boolean RightButton;
        public Vector2 RelativePosition;
        public Vector2 RelativeDelta;
        public Boolean LeftShiftPressed;
        public Boolean RightShiftPressed;
        public Boolean LeftCtrlPressed;
        public Boolean RightCtrlPressed;
        public Boolean LeftAltPressed;
        public Boolean RightAltPressed;


        public Boolean ShiftPressed { get { return this.LeftShiftPressed || this.RightShiftPressed; } }
        public Boolean CtrlPressed { get { return this.LeftCtrlPressed || this.RightCtrlPressed; } }
        public Boolean AltPressed { get { return this.LeftAltPressed || this.RightAltPressed; } }
    }

    public class MouseButtonEvent : MouseEvent
    {
        public MouseButton Button;
        public Boolean ButtonState;
    }

    public class MouseWheelEvent : MouseEvent
    {
        public Single WheelDelta;
    }

    public class ViewportDragDropArgs : DragDropArgs
    {
        /// <summary>
        /// The position that the item was dropped in relative coordiantes [0, 1].
        /// </summary>
        public Vector2 RelativePosition;
    }

    public class KeyboardEvent : EventArgs
    {
        public KeyboardState State;
    }

    // A viewport represents a viewing portal into the world
    // Debug drawing is a part of viewport
    public class Viewport : DockablePlugin
    {
        /// <summary>
        /// The type of message we send or receieve.
        /// </summary>
        enum MessageType : byte
        {
            ReadyForFrameBuffer,
            FrameBuffer,
            MouseEvent,
            MouseButtonEvent,
            MouseWheelEvent,
            KeyboardState,
            WorldToProjection,
            DrawLineSegment,
            LocalWindow,
            FramePassed,
        }

        public event EventHandler<MouseEvent> OnMouseMove;
        public event EventHandler<MouseButtonEvent> OnMouseDown;
        public event EventHandler<MouseButtonEvent> OnMouseUp;
        public event EventHandler<MouseWheelEvent> OnMouseWheel;
        public event EventHandler<ViewportDragDropArgs> OnDragDrop;
        public event EventHandler<KeyboardEvent> OnKeyboardState;

        private const Int32 BytesPerColor = sizeof(UInt16);

        /// <summary>
        /// Checks to see if the viewport is ready to send another frame. Typically
        /// when the viewport is ready, an event is sent out so that the user can render
        /// the viewport at the given width and height. However, in applications where
        /// the back buffer is acting as the viewport, we typically render on its own
        /// interval, and send the back buffer data when the viewport is ready.
        /// </summary>
        public Boolean IsReadyForFrame { get; private set; }

        /// <summary>
        /// Determines if this is a local viewport or not.
        /// Local viewports allow the game to render directly to them, for performance.
        /// </summary>
        public Boolean IsLocal { get; private set; }

        /// <summary>
        /// The name of the remote type we wish to instantiate.
        /// </summary>
        private const String RemoteType = "Skugo.StandardPlugins.Viewport";

        public Viewport(Client client, String displayName) :
            base(RemoteType, client, displayName)
        {
            // We're only local if the framework is running locally, and the client that is creating is is local
            this.IsLocal = Framework.Instance.IsLocal && client.IsLocal;
        }

        public void SetLocalWindow(IntPtr window)
        {
            if (this.IsLocal == false)
            {
                throw new Exception("Attempt to set a local window when we were not running locally!");
            }

            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a the message
            outgoing.Write((Byte)MessageType.LocalWindow);
            outgoing.Write((Int32)window);
            this.Send(stream.ToArray());
        }

        protected override void OnDrop(Byte[] data, Plugin source, Object sourceData)
        {
            if (this.OnDragDrop != null)
            {
                var incoming = data.ToBinaryReader();

                var x = incoming.ReadSingle();
                var y = incoming.ReadSingle();

                this.OnDragDrop(this, new ViewportDragDropArgs()
                {
                    Source = source,
                    SourceData = sourceData,
                    RelativePosition = new Vector2(x, y)
                });
            }
        }

        private void WriteDebugDrawItem<T>(BinaryWriter outgoing, DebugDrawItem<T> item) where T : DebugDrawItem<T>
        {
            outgoing.Write(item.ColorInternal.PackedValue);
            outgoing.Write(item.DurationInternal);
            outgoing.Write(item.PriorityInternal);
            outgoing.Write((Byte)item.SpaceInternal);
        }

        public void Draw(LineSegment line)
        {
            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a the message
            outgoing.Write((Byte)MessageType.DrawLineSegment);
            this.WriteDebugDrawItem(outgoing, line);
            outgoing.WriteVector3(line.P1);
            outgoing.WriteVector3(line.P2);
            outgoing.Write(line.ArrowHead1Size);
            outgoing.Write(line.ArrowHead2Size);

            this.Send(stream.ToArray());
        }

        /// <summary>
        /// Set the current world to projection matrix (used for debug draw).
        /// This matrix should match the matrix used to render the scene that is uploaded.
        /// </summary>
        /// <param name="worldToProj">The world to projection matrix (concatenation).</param>
        public void SetWorldToProjectionMatrix(Matrix worldToProj)
        {
            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a the message
            outgoing.Write((Byte)MessageType.WorldToProjection);
            outgoing.WriteMatrix(worldToProj);
            this.Send(stream.ToArray());
        }

        /// <summary>
        /// Inform the viewport that a frame has passed.
        /// </summary>
        public void FramePassed()
        {
            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a the message
            outgoing.Write((Byte)MessageType.FramePassed);
            this.Send(stream.ToArray());
        }



        /// <summary>
        /// Upload the frame buffer so it becomes visible to the editor.
        /// </summary>
        /// <param name="width">The width of the frame buffer.</param>
        /// <param name="height">The height of the frame buffer.</param>
        /// <param name="frameBuffer">Color data for the frame buffer. The expected order is RGBA, RGBA, RGBA...</param>
        public void UploadFrameBuffer(Int32 width, Int32 height, Byte[] frameBuffer)
        {
            // Make sure the frame buffer is of the correct size
            if (frameBuffer.Length != width * height * BytesPerColor)
            {
                throw new Exception("The frame buffer was not of the correct size. Make sure its size is width * height * RGBx (4 bytes)");
            }

            if (this.IsReadyForFrame == false)
            {
                throw new Exception("Attempting to upload the frame when the viewport was not yet ready." +
                    "Be sure to check the 'IsReadyForFrame' property on the viewport plugin).");
            }

            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a the frame buffer message
            outgoing.Write((Byte)MessageType.FrameBuffer);
            outgoing.Write(width);
            outgoing.Write(height);

            outgoing.Write(frameBuffer);
            this.Send(stream.ToArray());

            // We sent a frame, so we are no longer ready
            //this.IsReadyForFrame = false;
        }

        [DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)]
        public static extern IntPtr memcpy(IntPtr dest, IntPtr src, UIntPtr count);


        /// <summary>
        /// Upload the frame buffer so it becomes visible to the editor.
        /// </summary>
        /// <param name="width">The width of the frame buffer.</param>
        /// <param name="height">The height of the frame buffer.</param>
        /// <param name="frameBuffer">Color data for the frame buffer. The expected order is RGBA, RGBA, RGBA...</param>
        public unsafe void UploadFrameBuffer(Int32 width, Int32 height, Byte* frameBuffer)
        {
            if (this.IsReadyForFrame == false)
            {
                throw new Exception("Attempting to upload the frame when the viewport was not yet ready." +
                    "Be sure to check the 'IsReadyForFrame' property on the viewport plugin).");
            }

            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a the frame buffer message
            outgoing.Write((Byte)MessageType.FrameBuffer);
            outgoing.Write(width);
            outgoing.Write(height);
            
            long currentPosition = stream.Position;
            long dataLength = width * height * BytesPerColor;
            stream.SetLength(currentPosition + dataLength);

            fixed (Byte* data = stream.GetBuffer())
            {
                memcpy((IntPtr)(data + currentPosition), (IntPtr)frameBuffer, (UIntPtr)dataLength);
            }

            this.Send(stream.ToArray());

            // We sent a frame, so we are no longer ready
            //this.IsReadyForFrame = false;
        }

        private static void ReadMouseEvent(BinaryReader incoming, MouseEvent e)
        {
            e.LeftButton = incoming.ReadBoolean();
            e.MiddleButton = incoming.ReadBoolean();
            e.RightButton = incoming.ReadBoolean();

            e.LeftShiftPressed = incoming.ReadBoolean();
            e.RightShiftPressed = incoming.ReadBoolean();
            e.LeftCtrlPressed = incoming.ReadBoolean();
            e.RightCtrlPressed = incoming.ReadBoolean();
            e.LeftAltPressed = incoming.ReadBoolean();
            e.RightAltPressed = incoming.ReadBoolean();

            e.RelativePosition.X = incoming.ReadSingle();
            e.RelativePosition.Y = incoming.ReadSingle();
            e.RelativeDelta.X = incoming.ReadSingle();
            e.RelativeDelta.Y = incoming.ReadSingle();
        }

        // Called when packet data is received from our external counterpart
        protected override void OnReceive(Byte[] data)
        {
            var incoming = data.ToBinaryReader();

            var messageType = (MessageType)incoming.ReadByte();

            switch (messageType)
            {
                case MessageType.ReadyForFrameBuffer:
                {
                    this.IsReadyForFrame = true;
                    break;
                }

                case MessageType.MouseButtonEvent:
                {
                    var e = new MouseButtonEvent();
                    e.ButtonState = incoming.ReadBoolean();
                    e.Button = (MouseButton)incoming.ReadByte();
                    ReadMouseEvent(incoming, e);

                    if (e.ButtonState == true)
                    {
                        if (this.OnMouseDown != null)
                        {
                            this.OnMouseDown(this, e);
                        }
                    }
                    else
                    {
                        if (this.OnMouseUp != null)
                        {
                            this.OnMouseUp(this, e);
                        }
                    }
                    break;
                }
                case MessageType.MouseWheelEvent:
                {
                    var e = new MouseWheelEvent();
                    e.WheelDelta = incoming.ReadSingle();
                    ReadMouseEvent(incoming, e);

                    if (this.OnMouseWheel != null)
                    {
                        this.OnMouseWheel(this, e);
                    }
                    break;
                }
                case MessageType.MouseEvent:
                {
                    var e = new MouseEvent();
                    ReadMouseEvent(incoming, e);

                    if (this.OnMouseMove != null)
                    {
                        this.OnMouseMove(this, e);
                    }
                    break;
                }

                case MessageType.KeyboardState:
                {
                    var keyCount = incoming.ReadInt32();
                    var keys = new Keys[keyCount];

                    for (Int32 i = 0; i < keyCount; ++i)
                    {
                        keys[i] = (Keys)incoming.ReadByte();
                    }

                    var e = new KeyboardEvent();
                    e.State = new KeyboardState(keys);
                    if (this.OnKeyboardState != null)
                    {
                        this.OnKeyboardState(this, e);
                    }
                    break;
                }

                default:
                    throw new Exception("Unhandled message type!");
            }
        }
    }
}
